/**
 * Copyright 2011 Mathieu Magnaudet
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package edu.enac.geoapp.services;

import java.awt.Color;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.factory.GeoTools;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureCollections;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.JTS;
import org.geotools.map.DefaultMapLayer;
import org.geotools.map.MapLayer;
import org.geotools.referencing.CRS;
import org.geotools.styling.Font;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.SLD;
import org.geotools.styling.SLDParser;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.geotools.styling.StyleFactory;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.FilterFactory;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Polygon;

import edu.enac.geoapp.model.DisplayableForecast;
import edu.enac.geoapp.parser.grib.GribParser;
import edu.enac.geoapp.wims.exception.WimsException;
import edu.enac.geoapp.wims.model.ForecastSet;
import edu.enac.geoapp.wims.model.Product;
import edu.enac.geoapp.wims.model.WeatherProduct;

/**
 * Utility class that provides the different kinds of map layer
 * 
 * @author mathieu
 * 
 */
public class LayerProvider {
	private static final LayerProvider INSTANCE = new LayerProvider();
	private static List<Style> styleList;
	private static StyleFactory styleFactory = CommonFactoryFinder
			.getStyleFactory(null);
	private static CRSProvider crsProvider = CRSProvider.getInstance();
	private FileStore dataManager = FileStore.getInstance();
	private DataProvider dataProvider = DataProvider.getInstance();

	public static LayerProvider getInstance() {
		return INSTANCE;
	}

	private LayerProvider() {
		StyleBuilder sb = new StyleBuilder();
		styleList = new ArrayList<Style>();
		styleList.add(sb.createStyle(sb.createPolygonSymbolizer(Color.GREEN,
				Color.BLACK, 1)));
		styleList.add(sb.createStyle(sb.createPolygonSymbolizer(Color.YELLOW,
				Color.BLACK, 1)));
		styleList.add(sb.createStyle(sb.createPolygonSymbolizer(Color.GRAY,
				Color.BLACK, 1)));
		styleList.add(sb.createStyle(sb.createPolygonSymbolizer(Color.ORANGE,
				Color.BLACK, 1)));
		styleList.add(sb.createStyle(sb.createPolygonSymbolizer(Color.RED,
				Color.BLACK, 1)));
	}

	/**
	 * Returns an array of {@link MapLayer} with wind data from a grib file 
	 * @return
	 * @throws MismatchedDimensionException
	 * @throws NoSuchAuthorityCodeException
	 * @throws FactoryRegistryException
	 * @throws IOException
	 * @throws FactoryException
	 * @throws TransformException
	 * @throws SchemaException
	 */
	public MapLayer[][] getTempData() throws MismatchedDimensionException,
			NoSuchAuthorityCodeException, FactoryRegistryException,
			IOException, FactoryException, TransformException, SchemaException {
		return getGribLayers(dataManager.getPathToTempFile());
	}

	/**
	 * Returns an array of {@link MapLayer} with temperature data from a grib file
	 * @return
	 * @throws MismatchedDimensionException
	 * @throws NoSuchAuthorityCodeException
	 * @throws FactoryRegistryException
	 * @throws IOException
	 * @throws FactoryException
	 * @throws TransformException
	 * @throws SchemaException
	 */
	public MapLayer[][] getWindData() throws MismatchedDimensionException,
			NoSuchAuthorityCodeException, FactoryRegistryException,
			IOException, FactoryException, TransformException, SchemaException {
		return getGribLayers(dataManager.getPathToWindFile());
	}

	/**
	 * Returns a list of {@link DisplayableForecast} with containing meteorological objects from a GML file
	 * @return
	 */
	public List<DisplayableForecast> getWimsData() {
		try {
			return getWimsForecast(dataManager.getPathToMtoObjectsFile(), false);
		} catch (NoSuchAuthorityCodeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (FactoryException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (WimsException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Builds a list of map layer from a wims file with the projection EPSG:4326
	 * 
	 * @param path
	 *            the path to the wims file
	 * @return a list of map layer
	 * @throws NoSuchAuthorityCodeException
	 * @throws FactoryException
	 * @throws IOException
	 * @throws WimsException
	 */
	private List<DisplayableForecast> getWimsForecast(String path,
			boolean rotated) throws NoSuchAuthorityCodeException,
			FactoryException, IOException, WimsException {
		if (path == null)
			return null;

		List<DisplayableForecast> previsionList = new ArrayList<DisplayableForecast>();

		SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
		builder.setName("WIMS");
		builder.setCRS(rotated ? crsProvider.getRotatedCRS() : crsProvider
				.getStandardCRS());
		builder.add("WIMS", Polygon.class);
		final SimpleFeatureType WIMS = builder.buildFeatureType();

		WeatherProduct weatherProduct = dataProvider.getMeteoObjects(path);
		List<ForecastSet> forecasts = weatherProduct.getForecastSetList();
		SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(WIMS);
		for (ForecastSet f : forecasts) {

			List<FeatureCollection<SimpleFeatureType, SimpleFeature>> collectionList = new ArrayList<FeatureCollection<SimpleFeatureType, SimpleFeature>>();
			for (int i = 0; i < weatherProduct.getProductType()
					.getNbOfIntensityLevel(); i++) {
				collectionList.add(FeatureCollections.newCollection());
			}

			for (Product p : f.getWeatherProductList()) {
				featureBuilder.add(p.getPolygon().getPolygon());
				SimpleFeature feature = featureBuilder.buildFeature(null);
				collectionList.get(p.getIntensityLevel() - 1).add(feature);
			}
			DisplayableForecast prevision = new DisplayableForecast();
			int i = 0;
			for (FeatureCollection<SimpleFeatureType, SimpleFeature> coll : collectionList) {
				if (!coll.isEmpty()) {
					MapLayer layer = new DefaultMapLayer(coll, styleList.get(i));
					layer.setTitle("wims" + coll.getID());
					prevision.addLayer(layer, f.getMetada());
				}
				i++;
			}
			previsionList.add(prevision);
		}

		return previsionList;
	}

	private MapLayer[][] getGribLayers(String path)
			throws MismatchedDimensionException, NoSuchAuthorityCodeException,
			IOException, FactoryException, FactoryRegistryException,
			TransformException, SchemaException {
		GribParser parser = new GribParser();
		int type;
		if (path.contains("UV"))
			type = GribParser.UV;
		else
			type = GribParser.T;
		parser.parse(path, type);
		GridCoverage2D[][] gridArray = parser.getData();

		URL xml = null;
		if (type == GribParser.T)
			xml = LayerProvider.class.getResource("/sld_grid_T.xml");
		else
			xml = LayerProvider.class.getResource("/sld_grid_UV.xml");
		SLDParser styleReader = new SLDParser(styleFactory, xml);
		Style[] style = styleReader.readXML();

		int nbOfDate = gridArray.length;
		int nbOfIsobaric = gridArray[0].length;

		MapLayer[][] layerArray = new MapLayer[nbOfDate][nbOfIsobaric];
		for (int i = 0; i < nbOfDate; i++) {
			for (int j = 0; j < nbOfIsobaric; j++)
				layerArray[i][j] = new DefaultMapLayer(gridArray[i][j],
						style[0]);
		}

		return layerArray;
	}

	/**
	 * Returns a map layer built from a shape file
	 * 
	 * @param path
	 * @return
	 * @throws IOException
	 * @throws FactoryException
	 * @throws TransformException
	 * @throws MismatchedDimensionException
	 */
	public MapLayer getShapefileLayer(String path, Color fillColor,
			Color borderColor, boolean rotated) throws IOException,
			FactoryException, MismatchedDimensionException, TransformException {
		MapLayer mapLayer = null;
		File f = new File(path);
		if (!f.exists())
			throw new FileNotFoundException("Enable to find file " + path);

		StyleBuilder sb = new StyleBuilder();
		PolygonSymbolizer ps = sb.createPolygonSymbolizer(fillColor,
				borderColor, 1);
		Style solstyle = sb.createStyle(ps);

		ShapefileDataStore store = new ShapefileDataStore(f.toURI().toURL());
		String name = store.getTypeNames()[0];

		if (rotated) {
			FeatureCollection<SimpleFeatureType, SimpleFeature> worldCollection = exportShapefile(store);
			mapLayer = new DefaultMapLayer(worldCollection, solstyle);
		} else {
			FeatureSource<SimpleFeatureType, SimpleFeature> worldSource = store
					.getFeatureSource(name);
			mapLayer = new DefaultMapLayer(worldSource, solstyle);
		}

		return mapLayer;
	}

	private FeatureCollection<SimpleFeatureType, SimpleFeature> exportShapefile(
			ShapefileDataStore store) throws IOException, FactoryException,
			MismatchedDimensionException, TransformException {

		// creation de la transformation pour la reprojection
		CoordinateReferenceSystem sourceCRS = store.getSchema()
				.getCoordinateReferenceSystem();
		CoordinateReferenceSystem targetCRS = crsProvider.getRotatedCRS();
		boolean lenient = true;
		MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS,
				lenient);

		// ancienne et future collection
		FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection = store
				.getFeatureSource().getFeatures();
		FeatureCollection<SimpleFeatureType, SimpleFeature> newCollection = FeatureCollections
				.newCollection();

		// on itère sur les éléments de la collection
		FeatureIterator<SimpleFeature> iterator = featureCollection.features();
		while (iterator.hasNext()) {
			// copy the contents of each feature and transform the geometry
			SimpleFeature feature = iterator.next();
			SimpleFeature copy = feature;

			Geometry geometry = (Geometry) feature.getDefaultGeometry();
			Geometry geometry2 = JTS.transform(geometry, transform);

			copy.setDefaultGeometry(geometry2);
			newCollection.add(copy);
		}
		return newCollection;
	}

	/**
	 * Builds a map layer from the available airport list
	 * 
	 * @return The map
	 * @throws NoSuchAuthorityCodeException
	 * @throws FactoryException
	 */
	public MapLayer getAirportsLayer(Color color, boolean rotated) {
		Map<String, com.vividsolutions.jts.geom.Point> airportMap = dataProvider
				.getAirports();
		CoordinateReferenceSystem crs = rotated ? crsProvider.getRotatedCRS()
				: crsProvider.getStandardCRS();
		SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
		builder.setName("airports");
		builder.setCRS(crs);
		builder.add("airports_coord", com.vividsolutions.jts.geom.Point.class);
		builder.add("airport_name", String.class);

		final SimpleFeatureType airports = builder.buildFeatureType();

		SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(airports);
		FeatureCollection<SimpleFeatureType, SimpleFeature> collection = FeatureCollections
				.newCollection();
		for (String name : airportMap.keySet().toArray(
				new String[airportMap.size()])) {
			featureBuilder.add(airportMap.get(name));
			featureBuilder.add(name);
			SimpleFeature feature = featureBuilder.buildFeature(null);
			collection.add(feature);
		}
		return new DefaultMapLayer(collection, createPointStyle(color));
	}

	private Style createPointStyle(Color color) {
		StyleFactory sf = CommonFactoryFinder.getStyleFactory(GeoTools
				.getDefaultHints());
		FilterFactory ff = CommonFactoryFinder.getFilterFactory(GeoTools
				.getDefaultHints());

		Font font = sf.getDefaultFont();
		font.setSize(ff.literal(10.0));
		return SLD.createPointStyle("Triangle", color, color, 1, 5,
				"airport_name", font);
	}

	/**
	 * Retourne une maplayer contenant le plan de vol
	 * 
	 * @param path
	 * @return
	 * @throws NoSuchAuthorityCodeException
	 * @throws FactoryException
	 */
	public MapLayer getFlightPlanLayer(boolean rotated) {
		LineString flightPlan = dataProvider.getPrimaryFlightPlan(dataManager
				.getPathToFlightPlan());
		CoordinateReferenceSystem crs = rotated ? crsProvider.getRotatedCRS()
				: crsProvider.getStandardCRS();
		SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
		builder.setName("flightPlan");
		builder.setCRS(crs);
		builder.add("plan", LineString.class);

		SimpleFeatureType plan = builder.buildFeatureType();

		SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(plan);
		FeatureCollection<SimpleFeatureType, SimpleFeature> collection = FeatureCollections
				.newCollection();
		featureBuilder.add(flightPlan);
		SimpleFeature feature = featureBuilder.buildFeature(null);
		collection.add(feature);

		return new DefaultMapLayer(collection, createFlightPlanStyle());
	}

	private Style createFlightPlanStyle() {

		return SLD.createLineStyle(Color.GREEN, 2);
	}

	/**
	 * Return the world image
	 * 
	 * @param fillColor
	 *            Color of the countries
	 * @param borderColor
	 *            Color of the borders
	 * @param rotated
	 * @return
	 * @throws MismatchedDimensionException
	 * @throws IOException
	 * @throws FactoryException
	 * @throws TransformException
	 */
	public MapLayer getWorldLayer(Color fillColor, Color borderColor,
			float opacity, boolean rotated)
			throws MismatchedDimensionException, IOException, FactoryException,
			TransformException {
		MapLayer mapLayer = null;
		File f = new File(dataManager.getPathToWorldFile());
		if (!f.exists())
			throw new FileNotFoundException("Enable to find file "
					+ dataManager.getPathToWorldFile());

		Style shpStyle = SLD
				.createPolygonStyle(borderColor, fillColor, opacity);

		if (rotated) {
			FeatureSource<SimpleFeatureType, SimpleFeature> worldCollection = rotate(dataProvider
					.getWorld());
			mapLayer = new DefaultMapLayer(worldCollection, shpStyle);
		} else {
			FeatureSource<SimpleFeatureType, SimpleFeature> worldSource = dataProvider
					.getWorld();
			mapLayer = new DefaultMapLayer(worldSource, shpStyle);
		}

		return mapLayer;
	}

	private FeatureSource<SimpleFeatureType, SimpleFeature> rotate(
			FeatureSource<SimpleFeatureType, SimpleFeature> world) {
		// TODO Auto-generated method stub
		return null;
	}
}
